Skip to content

Add external body storage for audit on sql - #6029

Merged
rbev merged 15 commits into
masterfrom
audit-external-bodies
Oct 30, 2026
Merged

rbev merged 15 commits into
masterfrom
audit-external-bodies

Conversation

@rbev

@rbev rbev commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Audit's SQL Server and PostgreSQL persisters discard oversized and binary message bodies. Oversized text retains a searchable prefix, but the body API cannot return the complete body. Error's SQL persisters already support external storage for these bodies.

Change

Reuse Error's FileSystem, Azure Blob, and S3 providers for Audit by extracting them into ServiceControl.Persistence.BodyStorage.

  • Keep small valid text bodies inline. Store oversized, binary, content-encoded, non-UTF-8, and NUL-containing bodies externally.
  • Retain a UTF-8-safe inline search prefix for oversized text and serve the complete body through the body API.
  • Await external writes before inserting message rows, including when disposing an abandoned ingestion batch.
  • Delete external bodies before removing expired rows or partitions. A storage failure leaves the database records available for the next sweep.
  • Provision storage during setup and register the existing disk-space custom check for FileSystem storage.
  • Update persistence manifests, test fixtures, development configuration, and the testing tool.

RavenDB behavior is unchanged. Existing TooLarge and NotText rows retain their previous API responses; discarded bodies cannot be recovered. The new external-body state uses the existing integer column, so no schema migration is needed.

Required configuration

External storage is required for Audit SQL Server and PostgreSQL instances. Existing instances must add ServiceControl.Audit/MessageBody/StorageType and the selected provider's settings before restarting after an upgrade.

<add key="ServiceControl.Audit/MessageBody/StorageType" value="FileSystem" />
<add key="ServiceControl.Audit/MessageBody/FileSystem/StoragePath" value="D:\ServiceControl.Audit\bodies" />

MaxBodySizeToStore becomes the inline threshold, not a limit on the complete stored body. Azure defaults to the audit-bodies container; S3 defaults to the audit-bodies/ key prefix.

Each instance must use a separate directory, container, or key prefix. Sharing a location can let one instance's retention process delete another instance's bodies.

Design constraints

Sharing the providers avoids duplicating their storage formats, compression, authentication, and stream handling. Error retains its existing configuration and behavior.

Audit retention deletes by hourly prefixes on SQL Server and daily prefixes on PostgreSQL. This preserves PostgreSQL's partition-drop path without scanning message rows. SQL Server only visits hours containing rows, so bodies orphaned by failed inserts into otherwise empty hours can require manual cleanup.

The current design and configuration are documented in Message body storage.

@rbev rbev self-assigned this Oct 20, 2026
@rbev
rbev marked this pull request as ready for review October 21, 2026 05:29
Comment thread docs/load-testing.md Outdated
Comment thread docs/message-body-storage.md
Comment thread docs/message-body-storage.md Outdated
Comment thread docs/message-body-storage.md
Comment thread docs/message-body-storage.md
Comment thread src/ServiceControl.Audit.Persistence.Tests/EFCore/EFRetentionTests.cs Outdated
Comment thread src/ServiceControl.Audit.Persistence.EFCore/Implementation/AuditDataStore.cs Outdated
Comment thread src/ServiceControl.Persistence.BodyStorage/FileSystemBodyStoragePersistence.cs Outdated
Comment thread src/ServiceControl.Persistence.BodyStorage/AzureBlobBodyStoragePersistence.cs Outdated
@johnsimons

Copy link
Copy Markdown
Member

So this would be a new feature?
Because if I correctly recall, Raven does the same, correct?

@johnsimons

Copy link
Copy Markdown
Member

So MaxBodySizeToStore is the setting that controls whether it is stored or not?

Comment thread docs/message-body-storage.md Outdated
Comment thread docs/message-body-storage.md Outdated

The unit differs by provider, so the prefix is part of `IAuditPartitionManager`:

- SQL Server deletes rows one hour at a time. The prefix is the hour, `yyyyMMddHH-`.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that internval configurable? Will it to chunks? Is that a native SQL server feature or an application query?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is goverened by the retention process, in PostgreSQL it drops partitions and in SQL server it deletes by hour. That same process first calls the body storage to delete that time range.

The background process doesn't actually run on that interval however, that's just the granularity it uses to delete the rows.

Comment thread docs/message-body-storage.md Outdated
- SQL Server deletes rows one hour at a time. The prefix is the hour, `yyyyMMddHH-`.
- PostgreSQL drops whole day partitions without scanning rows. The prefix is the day, `yyyyMMdd`, and it matches every hour of that day. A day is only dropped once all of it is older than the retention period, so no body is removed earlier than its row would be.

Prefixes must not be empty and must not contain wildcard or path characters. SQL Server only visits hours that have rows, so an unreferenced body in an hour with no rows stays until that location is cleaned manually. PostgreSQL visits every expired day partition, including pre-created partitions without rows, so it also cleans unreferenced bodies of those days.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How to the other implementation deal with retention?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When you say "the other implementation" do you mean error instance or raven?

Error instance uses a similar background job to delete expired archived rows, and it makes sure the body is removed before doing so. There's no way for it to delete "blocks" of data like audit does because the resolution of errors is not a time-based process and relies on user intervention.


An invalid value, or missing settings for the chosen type, still fails at startup. Setup creates the directory, container, or bucket.

Audit settings use the `ServiceControl.Audit/` prefix in the application settings file. Provider-specific validation matches the Error instance, whose settings use the `ServiceControl/` prefix.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it a conscious decision that primary and audit do not share the same body store?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the problem with sharing the large message storage is that you can no longer clean up unused bodies without cooperation between the instances that have different retention windows.

The implementation allows them to share the same storage account, but the bodies themselves are not shared.

Comment thread docs/message-body-storage.md
@johnsimons

Copy link
Copy Markdown
Member

@rbev the delimiter for the prefix needs to be set by the implementation, example for Azure Blobs, we want the prefix to be HOUR_TIMESTAMP/, for file system we want hourly folder structures ( HOUR_TIMESTAMP ) and for S3 it may just be HOUR_TIMESTAMP-.

Have a look at my implementation of this in https://github.com/Particular/ServiceControl/pull/5318/changes#diff-985fb65dab014cdcb60c6d03ef95370d1059107f6887c478fdd2c175f9decab1 and https://github.com/Particular/ServiceControl/pull/5318/changes#diff-44f054b50a09b2ab13d8985465c7db9b57fd103451e40fc3030ba889c28624e7R24.
The trick is that you need to pass the DateTime createdOn 😉

@rbev rbev changed the title Add external body storage for audit Add external body storage for audit on sql Oct 28, 2026
@rbev
rbev force-pushed the audit-external-bodies branch from 7d3f48d to 4f1dc57 Compare October 29, 2026 03:31
@rbev
rbev enabled auto-merge (squash) October 30, 2026 01:04
@rbev
rbev merged commit 4a09a56 into master Oct 30, 2026
168 of 173 checks passed
@rbev
rbev deleted the audit-external-bodies branch October 30, 2026 01:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants